package editor.syntaxhighlighter;

import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

import javax.swing.text.AbstractDocument;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * 
 * @author Mark Dessain
 */
public class TokenStateMachine 
{

    private String lineContent = "";
    private int lineLength = 0;
    private SyntaxType endSyntax = SyntaxType.NULL;

    private String currentString = "";
    private String previousString = "";

    private String currentCharacter = "";
    private String previousCharacter = "";
    private String nextCharacter = "";

    private int offset = 0;
    private int index = 0;
    
    private SyntaxType currentType = SyntaxType.NULL;

    private ArrayList<DefaultStyledDocument.ElementSpec> specs;
    

    private HashSet<String> keywords;
    public MutableAttributeSet attrs;
    private HashMap<SyntaxType, MutableAttributeSet> typeMap = new HashMap<SyntaxType, MutableAttributeSet>();
    private MutableAttributeSet normal;
    private MutableAttributeSet keyword;
    private MutableAttributeSet comment;
    private MutableAttributeSet quote;
    private MutableAttributeSet error;

    /**
     * @param lineContent content to be added onto the line
     * @param startSyntax the syntax at the start
     * @param doc the document
     * @param attrs attributes
     */
    public TokenStateMachine(String lineContent, SyntaxType startSyntax, JavaDocument doc, MutableAttributeSet attrs )
    {
        this.lineContent = lineContent;
        this.lineLength = lineContent.length();
        
        this.currentType = startSyntax;

        this.attrs = attrs;
        
        specs = new ArrayList<DefaultStyledDocument.ElementSpec>();

        keywords = new HashSet<String>();
        keywords.add( "abstract" );
        keywords.add( "boolean" );
        keywords.add( "break" );
        keywords.add( "byte" );
        keywords.add( "byvalue" );
        keywords.add( "case" );
        keywords.add( "cast" );
        keywords.add( "catch" );
        keywords.add( "char" );
        keywords.add( "class" );
        keywords.add( "const" );
        keywords.add( "continue" );
        keywords.add( "default" );
        keywords.add( "do" );
        keywords.add( "double" );
        keywords.add( "else" );
        keywords.add( "extends" );
        keywords.add( "false" );
        keywords.add( "final" );
        keywords.add( "finally" );
        keywords.add( "float" );
        keywords.add( "for" );
        keywords.add( "future" );
        keywords.add( "generic" );
        keywords.add( "goto" );
        keywords.add( "if" );
        keywords.add( "implements" );
        keywords.add( "import" );
        keywords.add( "inner" );
        keywords.add( "instanceof" );
        keywords.add( "int" );
        keywords.add( "interface" );
        keywords.add( "long" );
        keywords.add( "native" );
        keywords.add( "new" );
        keywords.add( "null" );
        keywords.add( "operator" );
        keywords.add( "outer" );
        keywords.add( "package" );
        keywords.add( "private" );
        keywords.add( "protected" );
        keywords.add( "public" );
        keywords.add( "rest" );
        keywords.add( "return" );
        keywords.add( "short" );
        keywords.add( "static" );
        keywords.add( "super" );
        keywords.add( "switch" );
        keywords.add( "synchronized" );
        keywords.add( "this" );
        keywords.add( "throw" );
        keywords.add( "throws" );
        keywords.add( "transient" );
        keywords.add( "true" );
        keywords.add( "try" );
        keywords.add( "var" );
        keywords.add( "void" );
        keywords.add( "volatile" );
        keywords.add( "while" );

        normal = new SimpleAttributeSet();
        StyleConstants.setForeground(normal, Color.black);

        comment = new SimpleAttributeSet();
        StyleConstants.setForeground(comment, Color.gray);

        keyword = new SimpleAttributeSet();
        StyleConstants.setForeground(keyword, Color.blue);

        quote = new SimpleAttributeSet();
        StyleConstants.setForeground(quote, Color.magenta);

        error = new SimpleAttributeSet();
        StyleConstants.setForeground(error, Color.red);

        typeMap.put(SyntaxType.MULTICOMMENT, comment);
        typeMap.put(SyntaxType.SINGLECOMMENT, comment);
        typeMap.put(SyntaxType.STRING, quote);
        typeMap.put(SyntaxType.NULL, normal);
        typeMap.put(SyntaxType.DELIMITER, normal);
        typeMap.put(SyntaxType.ERROR, error);
        typeMap.put(SyntaxType.KEYWORD, keyword);

    }

    /**
     * @return True if start of line, False otherwise
     */
    private boolean lineStart()
    {
        return (index == 0) ? true : false;
    }

    /**
     * @return True if end of line, False otherwise
     */
    private boolean lineEnd()
    {
        return (index == lineLength-1) ? true : false;
    }

    /**
     * @return True if tokens are connected, False otherwise
     */
    private boolean connectedToken()
    {
        return ((index - offset) == 0) ? true : false;
    }

    /**
     * Refresh of text
     */
    private void refresh()
    {

        previousString = lineContent.substring(offset, index);
        currentString = lineContent.substring(offset, index+1);
        currentCharacter = lineContent.substring(index, index + 1);

        if(!lineEnd())
        {
            nextCharacter = lineContent.substring(index + 1, index + 2);
        }
        else
        {
            nextCharacter = "";
        }

        if(!lineStart())
        {
            previousCharacter = lineContent.substring(index - 1, index);
        }
        else
        {
            previousCharacter = "";
        }

    }

    /**
     * @return True if a single comment is found, False otherwise
     */
    private boolean findSingleComment()
    {
          if(currentCharacter.equalsIgnoreCase("/") && nextCharacter.equalsIgnoreCase("/"))
          {
              
              if(!connectedToken())
              {
                  addToken();
              }

              currentType = SyntaxType.SINGLECOMMENT;
              return true;
          }

          return false;

    }

    /**
     * @return True if a multi comment start is found, False otherwise
     */
    private boolean findMultiCommentStart()
    {
          if(currentCharacter.equalsIgnoreCase("/") && nextCharacter.equalsIgnoreCase("*"))
          {

              if(!connectedToken())
              {
                  addToken();
              }

              currentType = SyntaxType.MULTICOMMENT;
              return true;

          }

          return false;
    }

    /**
     * @return True if a multi comment end is found, False otherwise
     */
    private boolean findMultiCommentEnd()
    {
          if(currentCharacter.equalsIgnoreCase("/") && previousCharacter.equalsIgnoreCase("*"))
          {

              if(!connectedToken())
              {
                  index++;
                  addToken();
                  index--;
              }

              currentType = SyntaxType.NULL;
              return true;

          }

          return false;
    }

    /**
     * @return True if a string start is found, False otherwise
     */
    private boolean findStringStart()
    {
        if(currentCharacter.equalsIgnoreCase("'") || currentCharacter.equalsIgnoreCase("\""))
        {

          if(!connectedToken())
          {
              addToken();
          }

          currentType = SyntaxType.STRING;
          return true;

        }
        return false;

    }

    /**
     * @return True if a string end is found, False otherwise
     */
    private boolean findStringEnd()
    {
        if(currentCharacter.equalsIgnoreCase("'") || currentCharacter.equalsIgnoreCase("\""))
        {

          if(!connectedToken())
          {
              index++;
              addToken();
              index--;
          }

          currentType = SyntaxType.NULL;
          return true;

        }

        return false;
    }

    /**
     * @return True if a delimiter is found, False otherwise
     */
    private boolean findDelimiter()
    {

        if(Character.isWhitespace(currentCharacter.charAt(0)) || ".;:{}()[]+-%<=>!&|^~".indexOf(currentCharacter) != -1)
        {

          if(!connectedToken())
          {
              addToken();
          }

          currentType = SyntaxType.DELIMITER;
          return true;

        }

        return false;
    }

    /**
     * @param token token
     * @return True if it is a keyword, False otherwise
     */
    private boolean findKeyword(String token)
    {
        return keywords.contains(token);
    }
    
    /**
     * Adds a token to the list
     */
    private void addToken()
    {
        if(currentType == SyntaxType.NULL && ( findKeyword(currentString) || findKeyword(previousString)))
        {
            currentType = SyntaxType.KEYWORD;
        }

        MutableAttributeSet attrs = new SimpleAttributeSet();
        attrs.addAttributes(typeMap.get(SyntaxType.valueOf(currentType.toString())).copyAttributes());

        attrs.addAttribute(AbstractDocument.ElementNameAttribute, currentType.toString());
        attrs.addAttribute("type", currentType.toString());

        String text2 = lineContent.substring(offset, index);
        attrs.addAttribute("string", text2);

      
        DefaultStyledDocument.ElementSpec parContent2 = new DefaultStyledDocument.ElementSpec(attrs, DefaultStyledDocument.ElementSpec.ContentType, text2.toCharArray(), 0, text2.length() );
        specs.add(parContent2);
        
        currentString = "";
        offset = index;
    }

    /**
     * @return the new Spec
     */
    public ArrayList<DefaultStyledDocument.ElementSpec> setPosition()
    {
        for(index = 0; index < lineLength; index++)
        {
            refresh();

            if(currentType == SyntaxType.MULTICOMMENT)
            {
                findMultiCommentEnd();
            }
            else if(currentType == SyntaxType.SINGLECOMMENT)
            {
            }
            else if(currentType == SyntaxType.STRING)
            {
                findStringEnd();
            }
            else
            {

                if(!(findSingleComment() || findMultiCommentStart() || findStringStart() || findDelimiter()))
                {
                    if(currentType != SyntaxType.NULL)
                    {
                        addToken();
                        currentType = SyntaxType.NULL;
                    }
                }

            }
        }


        if(currentType == SyntaxType.STRING)
        {
            currentType = SyntaxType.ERROR;
        }

        if(currentType == SyntaxType.MULTICOMMENT)
        {
            endSyntax = currentType;
        }
        
        addToken();

        return specs;
    }

    /**
     * @return the end token 
     */
    public SyntaxType getEndSyntax()
    {
        return endSyntax;
    }
}